package edu.umd.ion.wavemeter.server;

import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.openjpa.persistence.PersistenceException;

import edu.umd.ion.wavemeter.service.CalibrationResult;
import edu.umd.ion.wavemeter.service.CalibrationSettings;
import edu.umd.ion.wavemeter.service.ChannelEvent;
import edu.umd.ion.wavemeter.service.ChannelInfo;
import edu.umd.ion.wavemeter.service.ChannelRoutingInfo;
import edu.umd.ion.wavemeter.service.DynamicExposureParameters;
import edu.umd.ion.wavemeter.service.InterlockSettings;
import edu.umd.ion.wavemeter.service.NotAvailableException;
import edu.umd.ion.wavemeter.service.PIDSettings;
import edu.umd.ion.wavemeter.service.PIDSettingsInUse;
import edu.umd.ion.wavemeter.service.PatternRequest;
import edu.umd.ion.wavemeter.service.WavemeterData;
import edu.umd.ion.wavemeter.service.WavemeterDataMask;
import edu.umd.ion.wavemeter.service.WavemeterException;
import edu.umd.ion.wavemeter.service.WavemeterParameterException;
import edu.umd.ion.wavemeter.service.WavemeterParameters;
import edu.umd.ion.wavemeter.service.WavemeterService;
import edu.umd.ion.wavemeter.service.WavemeterSetException;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

/**
 * WavemeterServiceImpl handles requests from the browser applications:
 * it has functions to set wavemeter properties and returns the status
 * of the instrument and the meassured wavelength.
 * 
 * This class has to be thread safe as it can be invoked by Tomcat in
 * multiple concurrent threads.
 * 
 * @author monroe-lab
 *
 */
public class WavemeterServiceImpl 
	extends RemoteServiceServlet
	implements WavemeterService
{
	private static final long serialVersionUID = 35L;
	private final int NumVirtChannels = WavemeterConfiguration.NumVirtChannels();
	
	/**
	 *  Shutdownmode is set to true when a client shuts down the server.
	 *  This is used in order to free the WavemeterLibrary callback function.
	 */
	private boolean Shutdownmode = false;
	
	/**
	 *  A single instance of the Worker thread class. The run() function
	 *  of this class runs the event loop which handles the callback notifications
	 *  from the WavemeterLibrary. 
	 */
	private static PollingThread Worker;
	private static DataBackend MyDataBackend;
	private static PIDCourseOracle MyPIDCourseOracle;
	
	/**
	 *  If the Worker is not existant initialize the Worker thread.
	 */
	public WavemeterServiceImpl()
	{
		if (MyDataBackend==null){
			MyDataBackend = new DataBackend();
		}
		if (MyPIDCourseOracle==null) {
			MyPIDCourseOracle = new PIDCourseOracle();
		}
		if (Worker==null) {
			startWorkerThread();
		}
	}

	String getCookieValue(String Name) {
		HttpServletRequest Request = getThreadLocalRequest();
		Cookie[] Cookies = Request.getCookies();
		for (int i=0; i<Cookies.length; ++i) {
			if (Cookies[i].getName().equals(Name)) {
				return Cookies[i].getValue();
			}
		}
		return new String();	
	}
	
	String getRemoteHost() {
		HttpServletRequest Request = getThreadLocalRequest();
		return Request.getRemoteHost();
	}

	String getRemoteAddr() {
		HttpServletRequest Request = getThreadLocalRequest();
		return Request.getRemoteAddr();
	}

	String getLocalAddr() {
		HttpServletRequest Request = getThreadLocalRequest();
		return Request.getLocalAddr();
	}

	String getUsername() {
		return getCookieValue(WavemeterParameters.Username);
	}

	
	/**
	 *  Request shutdown of the server process. This function should only be called
	 *  if the server process has to shutdown irreveribly. This frees the WavemeterLibrary
	 *  callback and shuts down the Worker thread. After this function is called the whole
	 *  server program (Tomcat) has to be restarted.
	 */
	synchronized public void shutdown()
	{
		Shutdownmode = true;
		Worker.expire();
	}
	
	
	synchronized private void startWorkerThread() {
		Worker = new PollingThread();
		Worker.setExposureParameters(MyDataBackend.getExposureParameters());
		Worker.registerPIDCourseOracle(MyPIDCourseOracle);
		try {
			for (int i=0; i<NumVirtChannels; ++i) {
				PIDSettings pid = MyDataBackend.getPIDSettings(i);
				pid.LockEnabled = false;
				Worker.setPID(pid);
				InterlockSettings ils = MyDataBackend.getInterlockSettings(i);
				Worker.setInterlock(ils);				
			}
		} catch (WavemeterException ignore) {}
		Worker.start();

	}
	/**
	 *  Ensures the Worker thread is running.
	 * @return always true
	 * @throws NotAvailableException after a call to shutdown()
	 */
	private boolean ensureWorkerAlive() throws NotAvailableException
	{
		synchronized (this) { if (Shutdownmode) throw new NotAvailableException(); } 
		boolean Alive = Worker.keepAlive();
		if (Alive) return true;
		startWorkerThread();
		return true;
	}

	/**
	 *  Sets the PID settings
	 *  @return PIDSettings as reported from the WavemeterLibrary after the requested values were set.
	 *  @throws WavemeterSetException 
	 */
	public PIDSettings setPID( PIDSettings settings ) throws WavemeterException
	{
		try {
			MyDataBackend.setPIDSettings(settings);
		} catch (PersistenceException e) {
			throw new WavemeterException(e.getMessage());
		}
		return Worker.setPID(settings);
	}
	
	/**
	 *  Sets the PIDCourse, the function or Wavelength the laser should follow.
	 *  @return The PIDCourse reported by the WavemeterLibrary after the requested Course was set.
	 *  @throws WavemeterException if the Course is not valid or the WavemeterLibrary SetPIDCourse
	 *  returns an error.
	 */	
	public String setPIDCourse( int Channel, String Course ) throws WavemeterException
	{
		try {
			PIDSettings s = MyDataBackend.getPIDSettings(Channel);
			s.Course = Course;
			MyDataBackend.setPIDSettings(s);
			return Worker.setPIDCourse(Channel, Course);
		} catch (PersistenceException e) {
			throw new WavemeterException(e.getMessage());
		}
	}
	
	/**
	 *  @return PIDSettings for the requested Channel as reported from the WavemeterLibrary.
	 *  @throws WavemeterException if the channel is out of range
	 */
	public PIDSettings getPID(int Channel) throws WavemeterException
	{
		PIDSettings MySettings = Worker.getPID(Channel);
		System.out.println("getPIDSettings "+Channel);
		return MySettings;
	}
	
	/**
	 *  Activates or deactivates the Channel
	 *  @throws WavemeterParameterException if the Channel is out of range
	 */
	public void setActive( int Channel, boolean Active ) throws WavemeterException
	{
		WavemeterParameters.checkChannel(Channel, "setActive");
		Worker.setActive(Channel, Active);
	}
	
	/**
	 *  Set the Auto Exposure mode for the channel
	 *  @throws WavemeterParameterException if channel is out of range
	 */
	public void setExposureMode(int channel, boolean mode) throws WavemeterException
	{
		WavemeterParameters.checkChannel(channel, "setExposureMode");
		Worker.setExposureMode(channel, mode);
	}

	/**
	 *  set the Exposure of the Channel and ccd-array to Value
	 *  ccd-array can be 1 or 2
	 *  @throws WavemeterParameterException if the channel is out of range
	 *  @throws WavemeterSetException if the WavemeterLibrary set-function fails
	 */
	public void setExposure(int Channel, int array, int Value) throws WavemeterException
	{
		WavemeterParameters.checkChannel(Channel, "setExposure");
		Worker.setExposure( Channel, array, Value);
	}

	/**
	 *  is meant to average the wavemeter reading
	 *  not implemented yet.
	 */
	public void setAverage(int Channel, int num)
	{
		
	}

	/**
	 *  Starts or Stops the actual Wavemeter (not only one client) 
	 *  @throws WavemeterSetException if the WavemeterLibrary set-function fails
	 */
	public void setMeasurement(boolean start) throws WavemeterSetException
	{
		Worker.setMeasurement(start);
	}

	/**
	 *  Enables or disables Wide mode
	 *  @throws WavemeterSetException if the WavemeterLibrary set-function fails
	 */
	public void setWideMode(boolean wide) throws WavemeterSetException
	{
		Worker.setWideMode(wide);
	}

	/**
	 *  Enables or disables Pulse mode
	 *  @throws WavemeterSetException if the WavemeterLibrary set-function fails
	 */
	public void setPulseMode(boolean pulsed) throws WavemeterSetException
	{
		Worker.setPulseMode(pulsed);
	}

	/**
	 *  Enables or disables Fast mode
	 *  @throws WavemeterSetException if the WavemeterLibrary set-function fails
	 */
	public void setFastMode(boolean fast) throws WavemeterSetException
	{
		Worker.setFastMode(fast);
	}

	/**
	 *  Requests the export of the pattern for the channels given in PatternRequest.
	 *  @throws WavemeterSetException if the WavemeterLibrary set-function fails
	 */
	public void setPattern(PatternRequest request) throws WavemeterSetException
	{
		Worker.setPattern(request);
	}
	
	/**
	 *  Enables the laser wavelength regulation
	 *  @throws WavemeterSetException if the WavemeterLibrary set-function fails
	 */
	public void setRegulationEnable(boolean regulate) throws WavemeterSetException
	{
		Worker.setRegulationEnable(regulate);
	}
	
	/**
	 *  Enables or disables switcher mode
	 *  @throws WavemeterSetException if the WavemeterLibrary set-function fails
	 */
	public void setSwitcherMode(boolean switcher) throws WavemeterSetException
	{
		Worker.setSwitcherMode(switcher);
	}

	/**
	 *  Clears the PID history of the channel.
	 */
	public void clearPIDHistory(int channel) throws WavemeterException
	{
		Worker.clearPIDHistory(channel);
	}
	
	/**
	 *  polls the Wavemeter status. This function is meant to be constantly polled.
	 *  It returns when a new value for all currently active Wavemeter channels
	 *  is available or after 5s. From the mask currently only the SpionAroundTime
	 *  is used to determine whether a new set of measurements is available.
	 *  @throws NotAvailableException if shutdown of the server process was requested before
	 */
	public WavemeterData pollWavemeter( WavemeterDataMask mask ) throws WavemeterException
	{
		ensureWorkerAlive();
		WavemeterData MyData = Worker.getData();
		if (MyData.SpinAroundTime<=mask.SpinAroundTime)
		{
			try {
				synchronized(Worker) {
					Worker.wait(3000);
				}
			}	
			catch( InterruptedException ignore )
			{			
			}
		}
		if (mask.PatternRequestList!=null && (!mask.PatternRequestList.isEmpty()) )
		{
			MyData.PatternList = Worker.getPatterns(mask.PatternRequestList);
		}
		return MyData;
    }
	
	protected void doGet(HttpServletRequest req, HttpServletResponse response)
    throws ServletException, java.io.IOException {
		try {
			String query = req.getQueryString();
			int Channel = Integer.valueOf(query);
			if (Channel<0 || Channel>=NumVirtChannels) 
				throw new java.io.IOException();

			ensureWorkerAlive();
			try {
				synchronized(Worker) {
					Worker.wait(5000);
				}
			}		
			catch( InterruptedException E )
			{			
			}
			WavemeterData MyData = Worker.getData();
			PrintWriter out = response.getWriter();
			out.println(MyData.ChannelData[Channel].Wavelength);
		}
		catch( Exception e )
		{
			response.sendError( HttpServletResponse.SC_NOT_FOUND );
		}
	}

	
	public void setSettings(String username, String settings) throws WavemeterException {
		try {
			MyDataBackend.setSettings(username, settings);
		} catch (PersistenceException e) {
			throw new WavemeterException(e.getMessage());
		}

	}
	
	public String getSettings(String username) throws WavemeterException { 
		String settings;
		try {
			settings = MyDataBackend.getSettings(username);
		} catch (PersistenceException e) {
			throw new WavemeterException(e.getMessage());
		}
		return settings;
	}	
	
	public String[] getUsernames() throws WavemeterException {
		String[] names;
		try {
			names = MyDataBackend.getUsernames();
		} catch (PersistenceException e) {
			throw new WavemeterException(e.getMessage());
		}
		return names;
	}
	
	public void setInterlock(InterlockSettings Interlock) throws WavemeterException {
		try {
			Worker.setInterlock(Interlock);
			MyDataBackend.setInterlockSettings(Interlock);
		} catch (PersistenceException e) {
			throw new WavemeterException(e.getMessage());
		}
	}
	
	public InterlockSettings getInterlock(int Channel) throws WavemeterException {
		InterlockSettings I = Worker.getInterlock(Channel);
		System.out.println("getInterlockSettings "+Channel);
		return I;
	}
	
	public CalibrationResult calibrate(int unit, double value, int channel ) throws WavemeterException {
		CalibrationResult r;
		try {
			String Username = getCookieValue(WavemeterParameters.Username);
			r = Worker.calibrate(unit, value, channel, Username, getRemoteHost()+" ("+getRemoteAddr()+")");
			MyDataBackend.addCalibration(r);
		} catch (PersistenceException e) {
			throw new WavemeterException(e.getMessage());
		}
		return r;
	}

	public CalibrationResult[] calibrationHistory() {
		return  MyDataBackend.getCalibrationHistory();
	}

	@Override
	public String[] getPIDCourseSuggestions(int Channel)
			throws WavemeterException {
		String[] s = MyPIDCourseOracle.getSuggestions(Channel);
		return s;
	}

	@Override
	public ChannelRoutingInfo[] channelRouting() {
		return Worker.channelRouting().toArray(new ChannelRoutingInfo[Worker.channelRouting().size()]);
	}

	@Override
	public void addChannelName(int Channel, String Name) throws WavemeterException {
		try {
			Worker.addChannelName(Channel, Name);
			MyDataBackend.setChannelInfo(Channel, Name);
		} catch (PersistenceException e) {
			throw new WavemeterException(e.getMessage());
		}
	}

	@Override
	public DynamicExposureParameters getExposureParameters()
			throws WavemeterException {
		DynamicExposureParameters P = (DynamicExposureParameters) Worker.getExposureParameters();
		P.setEditable(getRemoteAddr().equals(getLocalAddr()));
		return P;
	}

	public DynamicExposureParameters setExposureParameters(DynamicExposureParameters ExposureParams)
	throws WavemeterException {
		DynamicExposureParameters P;
		try {
			if (getRemoteAddr().equals(getLocalAddr())) {
				P = (DynamicExposureParameters) Worker.setExposureParameters(ExposureParams);
				MyDataBackend.setExposureParameters( P);
			} else {
				P = MyDataBackend.getExposureParameters();
			}
		} catch (PersistenceException e) {
			throw new WavemeterException(e.getMessage());
		}
		return P;
	}

	@Override
	public ChannelInfo[] channelInfo() throws WavemeterException {
		return MyDataBackend.getChannelInfo();
	}

	@Override
	public CalibrationSettings getCalibrationSettings(int Channel)
			throws WavemeterException {
		CalibrationSettings CalS = MyDataBackend.getCalibrationSettings(Channel);
		System.out.println("getCalibrationSettings "+Channel);
		return CalS;
	}

	@Override
	public void setCalibrationSettings(int Channel, CalibrationSettings Settings)
	throws WavemeterException {
		try {
			MyDataBackend.setCalibrationSettings(Settings);
		} catch (PersistenceException e) {
			throw new WavemeterException(e.getMessage());
		}
	}

	@Override
	public PIDSettingsInUse getPIDSettingsInUse() throws WavemeterException {
		return Worker.getPIDSettingsInUse();
	}

	@Override
	public Boolean isLocalClient() {
		return getRemoteAddr().equals(getLocalAddr());
	}

	@Override
	public int getNumberOfChannels() {
		return WavemeterConfiguration.NumVirtChannels();
	}

	@Override
	public int getNumberOfPIDChannels() {
		return WavemeterConfiguration.NumPIDChannels();
	}

	@Override
	public void raiseChannelEvent(ChannelEvent E) throws WavemeterException {
		Worker.raiseChannelEvent(E);		
	}

}
